﻿using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Dynamic;
using System.Linq.Expressions;
using System.Reflection;
using System.Windows.Input;

namespace DynamicMVVM
{
    public class DynamicViewModel : DynamicObject, INotifyPropertyChanged
    {
        private const string CommandConst = "Command";
        private const string ExecuteConst = "Execute";
        private const string CanExecuteConst = "CanExecute";

        readonly Dictionary<string, ICommand> _commands = new Dictionary<string, ICommand>();

        public override bool TryGetMember(GetMemberBinder binder, out object result)
        {
            if(binder.Name.EndsWith(CommandConst))
            {
                if(_commands.ContainsKey(binder.Name))
                {
                    result = _commands[binder.Name];
                    return true;
                }

                var ececuteMethodName = string.Format("{0}{1}", ExecuteConst, binder.Name.Replace(CommandConst, ""));
                var canEcecuteMethodName = string.Format("{0}{1}", CanExecuteConst, binder.Name.Replace(CommandConst, ""));

                var executeMethodInfo = GetType().GetMethod(ececuteMethodName);
                var canExecuteMethodInfo = GetType().GetMethod(canEcecuteMethodName);

                if (executeMethodInfo != null)
                {
                    var cmdCommand = GetCommand(executeMethodInfo, canExecuteMethodInfo);

                    _commands.Add(binder.Name, cmdCommand);

                    result = cmdCommand;
                    return true;
                }

            }
            return base.TryGetMember(binder, out result);
        }

        private ICommand GetCommand(MethodInfo executeMethodInfo, MethodInfo canExecuteMethodInfo)
        {
            if (!CanMakeGeneric(executeMethodInfo, canExecuteMethodInfo))
            {
                return new RelayCommand(
                    (obj) => InvokeExecuteMethod(executeMethodInfo, obj),
                    (obj) => InvokeCanExecuteMethod(canExecuteMethodInfo, obj));
            }
            else
            {
                var executeMethodParams = executeMethodInfo.GetParameters();
                var cmdParamType = executeMethodParams[0].ParameterType;
                var cmdType = typeof(RelayCommand<>);
                Type[] typeArgs = { cmdParamType };
                var genericType = cmdType.MakeGenericType(typeArgs);

                Action<object> executeMethod = (obj) => InvokeExecuteMethod(executeMethodInfo, obj);
                Predicate<object> canExecuteMethod = (obj) => InvokeCanExecuteMethod(canExecuteMethodInfo, obj);

                return Activator.CreateInstance(genericType, executeMethod, canExecuteMethod) as ICommand;
            }
        }

        private static bool CanMakeGeneric(MethodInfo executeMethodInfo, MethodInfo canExecuteMethodInfo)
        {
            if (canExecuteMethodInfo != null)
            {
                var executeMethodParams = executeMethodInfo.GetParameters();
                var canExecuteMethodParams = canExecuteMethodInfo.GetParameters();

                if( executeMethodParams.Length>0 
                    && executeMethodParams.Length == canExecuteMethodParams.Length
                    && executeMethodParams[0].ParameterType != typeof(object)
                    && executeMethodParams[0].ParameterType == canExecuteMethodParams[0].ParameterType)
                {
                    return true;
                }
            }
            return false;
        }

        private void InvokeExecuteMethod(MethodInfo methodInfo, object obj)
        {
            var methodParams = methodInfo.GetParameters();

            if (methodParams.Length == 0)
            {
                methodInfo.Invoke(this, null);
            }
            else
            {
                var parameters = new object[methodParams.Length];
                parameters[0] = obj;
                methodInfo.Invoke(this, parameters);
            }
        }

        private bool InvokeCanExecuteMethod(MethodInfo methodInfo, object obj)
        {
            if (methodInfo == null)
                return true;
            if(methodInfo.ReturnType != typeof(bool))
                return true;

            var methodParams = methodInfo.GetParameters();

            if (methodParams.Length == 0)
            {
                return (bool) methodInfo.Invoke(this, null);
            }
            else
            {
                var parameters = new object[methodParams.Length];
                parameters[0] = obj;
                return (bool) methodInfo.Invoke(this, parameters);
            }
        }

        public override IEnumerable<string> GetDynamicMemberNames()
        {
            return _commands.Keys;
        }

        #region Implementation of INotifyPropertyChanged

        public event PropertyChangedEventHandler PropertyChanged;


        protected void NotifyPropertyChanged(String info)
        {
            if (PropertyChanged != null)
            {
                PropertyChanged(this, new PropertyChangedEventArgs(info));
            }
        }

        protected void RaisePropertyChanged<T>(Expression<Func<T>> property)
        {
            if (PropertyChanged == null) return;

            var expression = property.Body as MemberExpression;
            if (expression == null) return;
            var member = expression.Member;
            PropertyChanged(this, new PropertyChangedEventArgs(member.Name));
        }
        #endregion
    }
    
}
